Vue 在官方文件 - Reactivity Fundamentals 中,正式進入 reactive()
和 ref()
之前,先提到了 nextTick()
這個 API。
第一次看的時候真的是一頭霧水。
學習 Vue 一段時間後回頭重看,終於知道這是在幹嘛,也發現自己當初缺失的那塊拼圖;所以這篇會先以新手的視角切入,整理在新手學習 nextTick()
之前,應該要了解的事情,再來才會去了解 nextTick()
。
nextTick()
和生命週期鉤子nextTick()
的用途nextTick()
<div>
<h2>今日訊息:{{ message }}</h2>
</div>
const message = ref("第 12 天,還行嗎?");
message
這筆資料,Vue 就會幫我們重新渲染:<div>
<h2>今日訊息:{{ message }}</h2>
</div>
const message = ref("第 12 天,還行嗎?");
setTimeout(() => {
message.value = "睏到不行";
}, 1000);
我們修改資料的步驟,就是流程圖中的紅色區塊(component reactive state/元件的響應狀態),剩下的事情由 Vue 處理。
所以,基本上,開發者在 Vue 框架下開發,可以專注處理資料的變化和邏輯,不需要自己操作 DOM,也就不太會用到一堆 document.querySelector
等。
那 DOM 的事情都改由 Vue 處理了,那如果在 DOM 變化的過程前後 (這裡泛指建立、更新、銷毀),我們想要做事怎麼辦?
這就是為什麼會提供 nextTick()
和生命週期鉤子(Hook)。
今天只會講到 nextTick()
,而他被呼叫的時間就在 Vue 幫我們更新 DOM 的時間點後,所以我們要先認識 Vue 什麼時候更新 DOM。
響應式資料更新後,Vue 會先同步更新相依數據,再以非同步的方式去更新 DOM。
這樣的好處是可以節省效能,如果開發者短時間內修改了好幾次的資料,其實 Vue 只需要渲染最終的結果,就能省去中間一直重新渲染 DOM 的效能。
数据的修改,同时会触发dom渲染,即执行一个同步修改数据任务,再执行一个dom渲染的异步任务,此时完成了一轮loop,如果此时去获取dom,那必然会在异步任务执行前获取,当然获取不到,想要获取更新后的dom,必须再一次开启一个时间循环,即使用nexttick。
所以說,更新渲染 DOM 這件事是非同步的,等於:不知道 DOM 什麼時候會完成更新
那如果你修改完資料之後,要拿新的 DOM 元素內狀態 (如:寬、高) 來做事怎麼辦?
所以 Vue 提供了 nextTick
這個 API,他被呼叫的時機,會是在 DOM 更新渲染完成後。
nextTick()
,等 nextTick()
被呼叫後,再接著處理欸所以那個 tick 到底是什麼?
其實就是所謂的「事件循環」
每次修改資料後,Vue 要幫忙做一堆工作,至少包括:
的確是蠻少用到的,除非需要拿到資料更新後的 DOM 狀態來做事,我一時之間也想不到什麼情境,在重新認識 Vue.js 裡有提到滾動的例子。
有一個有高度限制的 <div>
在裡面用 v-for 渲染 messages 清單,每次 <input>
內容送出後,會將輸入內容 push 到 messages 陣列中,內部會新增一個新的 message 清單項目 。
我們想要在每次新增後,都將 <div>
捲到最底部,才可以看到最新的輸入內容。
期望效果:
問題範例
<div class="messages" ref="messagesDiv">
<div v-for="message in messages" :key="message">{{ message }}</div>
</div>
<input
type="text"
v-model.trim="newMessage"
@keydown.enter="addToMessages"
/>
function addToMessages() {
messages.value.push(newMessage.value);
const messagesDiv = document.querySelector(".messages");
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
上面這段程式碼的呈現結果:
仔細觀察程式碼的畫面,會發現每次「捲到底部」都是停在上一次新增的內容地方,因為在這個事件迴圈中,拿到的 messagesDiv.scrollHeight
還是前一次的高度。
nextTick()
裡function addToMessages() {
messages.value.push(newMessage.value);
nextTick(() => {
const messagesDiv = document.querySelector(".messages");
messagesDiv.scrollTop = messagesDiv.scrollHeight;
});
}
await nextTick()
後面,等到接到 nextTick
完成再繼續執行async function addToMessages() {
messages.value.push(newMessage.value);
await nextTick();
const messagesDiv = document.querySelector(".messages");
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
使用到 nextTick 通常是為了取 DOM 元素,那在 Vue 3 要怎麼拿 DOM 元素比較好?
Vue 在 <tamplate>
提供了特殊的 ref 屬性,透過 ref 可以直接拿到渲染後的 DOM 元素的參照(reference)。
只要先在 <script>
中宣告和 <template>
中 ref 屬性同名的變數,就可以透過這個變數拿到 DOM。
import { ref } from 'vue'
const messages = ref(['牛肉湯', '肉燥飯', '鍋燒意麵', '白糖粿'])
const newMessage = ref('')
const messagesDiv = ref(null)
async function addToMessages() {
messages.value.push(newMessage.value)
await nextTick();
//在這裡取到的 messagesDiv 已經變成 DOM 元素了
messagesDiv.value.scrollTop = messagesDiv.value.scrollHeight
}
注意事項
注意要在元素渲染、掛載到 DOM 上之後,才能透過 ref
屬性取到該元件;所以一開始的變數(messagesDiv
)才會綁定 null
,因為 Vue 在讀取這段程式碼時,還在準備渲染,這時候還選不到 messagesDiv
這個元素。
平常很少需要把更新後的 DOM 狀態拿出來做邏輯處理,比較常見的情況,應該是瀑布流或是輪播圖,要在 API 拿資料回來後,根據新的 DOM 去計算位置等等。
但因為時間太趕,沒有找到相對應的範例,如果之後有踩到坑,會再補充。
今天的內容有點雜,整理重點如下:
nextTick()
。nextTick()
會在 DOM 非同步更新完成後被呼叫,在這個呼叫時機點,可以拿資料更新後的 DOM 狀態來做事ref
屬性,注意要在元素掛載上去後才能拿到。